ここまで9回、specの書き方を細かく書いてきました。最後に、それが今どういう意味を持つのかを書きます。
結論から書くと、時間をかけるべきはspecのレビューだと考えます。specは仕様そのものなので、間違っていればバグや障害になる。specも大量生産されるので、判断しやすいように可読性が重要になってきています。

時間をかけるべきはspecのレビュー

1回目に、実装している時もレビューが中心になった、という話を書きました。書くコストが下がった分、時間はレビューに寄っています。追いにくいコードのストレスが、そのまま生産性に効いてくる。

では何を見るのか。specです。

specが表しているのは「どういう時に、どう動くべきか」です。つまり仕様そのもの。実装が正しいかを判断する基準が、specに書かれています。

実装の方は動かせば確認できますし、仕様の誤認があってもレビューで気づけます。でもspecが間違っていると、間違った基準で緑になります。しかも暗黙の仕様(書かれていないが期待されている挙動)は、specに現れていなければ誰も気づけません。

これは人間が書いても同じです。AIが書くようになって量が増えた分、重要性が上がりました。

レビューで見ているのは、1回目に挙げた4つです。

  1. そのケースは既存と重複していないか
  2. 抜けている組み合わせはないか
  3. 1ケースの中で、検証すべき項目に漏れがないか
  4. その期待値は、仕様として正しいか

3で見るのは、成功したか失敗したか、何が変更されたか、何が呼び出されたか。書く側の検証項目(レスポンス、DBの変更、非同期処理の呼び出し)と、レビューする側の観点が対応しています。

だから、ここまで書いてきた書き方が効きます。テストパターンの宣言があれば1が確認しやすくなり、完全比較をしていれば3が確認しやすくなる。

実装→specの順を許容する

自分の書き方は、もともとこうでした。

  1. 最低限の正常系を実装して、手動で動作確認
  2. 正常系のspecを書く
  3. ケースを洗い出してcontextを組み立てる
  4. 検証を追加してから実装
  5. 全て通るまで繰り返す

何もないところに見通しは作り難いので、純粋なTDDではありません。ただ、観測点が最初から外側にあります。request specやmodelの公開インターフェースで見ているので、実装の内部構造は覗いていない。

AIに投げる時は、この順序を維持できません。先にspecを渡すと、そのspecが通る最小限の実装を書きます。仕様の全体像を持たないので、specに書かれていない部分が雑になる。

なので実装→specの順を許容しています。

代わりにリスクが上がります。実装を見てspecを書くので、実装をなぞるspecになりやすい。実装が間違っていれば、期待値も間違ったまま緑になります。

ここを、決めておいた書き方で吸収します。

  • テストパターンを先に宣言させる
  • 観測点は公開インターフェース
  • mockは境界だけ
  • have_attributeseqで完全比較

これが守られていれば、実装をなぞるspecにはなりにくい。順序の問題を書き方で吸収する形です。

AIは書かれていない判断を埋め込む

9回目にcaseelseでraiseする話を書きました。AIに実装させていると、これが効いてきます。

AIはelseを書かないことが多い。 指示された分岐だけ書いて、漏れたケースはnilを返す。落ちないので気づけません。

もっと厄介なのは、分岐が2つの時にelseに正常系を書かれるケースです。

if download.model.to_sym == :member
  # メンバーの処理
else
  # スペースの処理をここに書かれる
end

動きます。レビューしても違和感がない。

ただ、これは「どれにも当てはまらなかった時はスペースとして扱う」という仕様を決めたことになっています。コードにはそう書いてある。でも誰もその判断をしていません。AIは「2つあるから片方をelseにする」という構造上の都合で書いただけです。

3つ目が増えた時に顕在化しますが、その時には「これがデフォルトだったのか」と誤読されます。

レビューでは見つけられない

elseに妥当な処理が書かれていれば、読んで違和感はありません。動作も正しい。間違っていないので指摘の根拠がない。

気づくには「これは網羅なのかデフォルトなのか」を毎回問う必要があります。レビュアーの注意力に依存する。

だからcase+elseでraiseが効きます。elseがraiseで埋まっていれば、デフォルトを置く場所がありません。デフォルトが必要なら明示的に書き換えることになり、その時初めて「デフォルトを決める」という判断が発生して、diffに出ます。

判断がコードの形に現れる。 今は「判断していない」と「デフォルトを決めた」が同じ見た目になっているのが問題です。

握り潰して解決しようとする

アラート対応を任せた時にも、同じことがありました。例外をrescueして握り潰し、通知を止めようとする。

AIにとっては「エラーが出なくなった」ので解決です。でも原因は残ったままで、しかも次に起きても誰も気づけなくなります。「この例外は無視していい」という判断を、誰も下していないのに埋め込んでいる。

過去の経緯はコミットしない

もう1つ、AIに書かせていて気になるのが回帰テストを足したがることです。

バグを直した箇所に「念のため」のケースが追加される。仕様変更の時には、変更前後を比べるケースが追加される。コストがゼロなので、判断せずに足せてしまいます。

どちらも、確認のために書くのは構いません。ただコミットする必要はないと思っています。

バグ対応のケースは、コードだけ見ても理由が読めません。そこだけ粒度が違う。リファクタで消していいか判断できず、結果的に誰も触らなくなる。

仕様変更のケースはもっと明確です。過去の仕様をテストに残すと、今後の実装を縛ります。 変更のたびに履歴が溜まって、実装を変えるたびに経緯を確認することになる。テストが変更を妨げる方向に働きます。

残すとすれば、それは回帰テストではなく現在の仕様として書けるはずです。「このバグが再発しないこと」ではなく「この条件ではこう動く」と書けるなら、普通のケースとして残せばいい。

「再発したらどうする」という反論はあると思います。ただ、仕様として書けないバグというのは、実装の特定の組み合わせでしか起きないものです。それはテストで蓋をするより、実装を直す方が筋が通ります。テストを足すと、歪んだ実装がそのまま残ります。

そして「念のため」でケースが積み上がると、8回目に書いた「実装を根拠にケースを絞る」が成立しなくなります。何のためにあるか分からないケースは、減らす判断もできません。

参考にしたspecがやばいと、作られるspecもやばい

AIは既存コードを参照して書きます。1ファイル悪いspecがあれば、それが増殖する。

毎回指示して守らせるより、既存コードを直す方が安い。 しかも既存コードは毎回読まれますが、プロンプトは忘れられます。

今リファクタする理由はここにあります。コードが増える前に手本を整えておく。3回目に書いた「後から直せない」と同じ話です。

既に大量にある場合

とはいえ、全部は直りません。直している間も増えます。

現実的には、AIが参照する範囲を制御する方向になります。

手本を1つ作って明示する。 「新しいspecはこのファイルを参考に書く」とCLAUDE.mdやAGENTS.mdのような指示ファイルに書く。既存の悪いspecが残っていても、参照先を指定すれば影響を減らせます。完全ではありませんが、何も指定しないよりはるかにいい。

触ったファイルだけ直す。 改修で開いたspecは決めた形に合わせる。新規は最初からその形で書く。時間はかかりますが、触られる頻度が高いファイルから直るので効率はいい。触られないファイルは、AIが参照する確率も低い。

ディレクトリ単位で揃える。 model specから揃える、など。まとまった範囲が揃っていれば、AIがそこを参照する確率が上がります。虫食いだと参照先が運になる。

最後に残るもの

構造を整えても、書き方を決めても、減らないものがあります。

その期待値が、仕様として正しいか。

カバレッジは通ったか、しか見ていません。完全比較は変化を検知するだけで、期待値が正しいかは判定しない。eq(403)の403が正しいかは、仕様を知らないと分かりません。

AIが書くspecが危ないのはここです。実装から期待値を起こすので、実装が間違っていれば期待値も間違います。緑になる。構造も書き方も守られている。それでも仕様と違う。

ここだけは人間が判断するしかない。

だから今までの9回は、全部そのための準備だと思っています。構造を整えてケースの重複と漏れを見やすくし、itをまとめて検証項目を一望できるようにする。完全比較とmatcherの選び方で検知漏れを減らし、テストデータと責務の分け方で無駄なケースと時間を削る。そして仕組みで書き忘れを落とす。

動くかではなく、仕様として正しいか。 そこに人間の時間を集中させるための手段です。

これから

この内容をルール化して、AIに適用していく予定です。

記事をそのままルールにはできません。記事は「なぜそうするか」に価値がありますが、ルールは「何をするか」だけでいい。理由まで書くと長くなります。指示ファイルはコンテキストを消費するので、他のルールが入らなくなる。どこまで残すかは試しながら決めることになりそうです。

そして一度で終わる作業でもありません。ルールを適用してspecを生成し、レビューして、ルールを直す。この繰り返しになります。今回の記事も、具体を見ないと言語化できませんでした。ルールも同じで、レビューの産物として育つものだと思っています。

コードベースや規約がAIに対するハーネスになるという話は、コードベースと規約をハーネスとして考えてみたで書きました。ルール化の実際は、また別の記事にします。

「変更に強いテストを書く」のまとめ

全部に共通しているのは1つで、リファクタでは落ちず、仕様が変わったら落ちる。その非対称を作るための具体でした。

そしてもう1つ、specの可読性の価値が上がっています。書くのはAIでも、仕様として正しいかを判断するのは人間です。読めないspecは判断できない。速く書けるようになった分、ストレスなく読めることの重要性が上がっていると感じています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です